Skip to content

feat(api): stage temporary artifacts and Codex schema output in Effect scoped temp directories (FileSystem phase 1, module 2) - #508

Merged
ScriptedAlchemy merged 5 commits into
mainfrom
feat/effect-filesystem-phase1-tempdirs
Sep 4, 2026
Merged

feat(api): stage temporary artifacts and Codex schema output in Effect scoped temp directories (FileSystem phase 1, module 2)#508
ScriptedAlchemy merged 5 commits into
mainfrom
feat/effect-filesystem-phase1-tempdirs

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Phase 1 of the Effect FileSystem / Path adoption, module 2, on top of #501 (the scaffolder pilot + convention flip, merged as 4c911b0).

What changes

Two ordinary temporary directories in packages/agent-bundle — the ones the design lists as safe to move — go from mkdtemp + try/finally rm to the withTempDirectory bracket over Effect FileSystem:

Site (before) After
src/api.ts temporaryArtifactmkdtemp(join(os.tmpdir(), '.agent-bundle-artifact-')), finally rm(..., { recursive, force }); backs listMcp / invokeMcp / runMcp / listHooks / simulateHook when the caller passes no artifact runWithPlatform(withTempDirectory({ directory: resolve(root), prefix: '.agent-bundle-artifact-' }, artifact => ...)): the existing build(...) + callback lifted with liftPromise; the bracket does the rm (recursive, force) on success, failure and interruption
src/host-contracts/codex-plugin-validation.ts schemaGenerationDiagnosticsmkdtemp(join(os.tmpdir(), 'agent-bundle-codex-schema-')), finally rm(...) around codex debug schema, then readFile of the emitted hooks.schema.json schemaGenerationDiagnostics is an Effect program (withTempDirectory, child process still runBoundedChildProcess via liftPromise, schema read via fs.readFileString); the validator's validateCodexPlugin edge runs it with runWithPlatform

New: src/effect/platform.ts — the framework's platform layer, built so the dev server can reuse it in phase 2:

  • platformLayer — the NodeServices union (ChildProcessSpawner | Crypto | FileSystem | Path | Stdio | Terminal) composed exactly as NodeServices.layer composes it, but from @effect/platform-node-shared, the package that implements those services (@effect/platform-node's NodeFileSystem etc. are re-exports). Reason — consumer install footprint, measured by installing the packed tarball into a fresh project: main 273 MB / 98 packages; with @effect/platform-node 296 MB / 115 packages (undici, mime, and a Redis client — rc.112 declares redis as a non-optional peer, so npm auto-installs redis + @redis/*, 16 MB); with @effect/platform-node-shared 277 MB / 104 packages (@types/node, @types/ws, undici-types). create-agent-bundle bundles its dependencies and keeps NodeServices.layer. PlatformServices is derived from the layer (Layer.Success<typeof platformLayer>), so no effect/unstable/* import is needed for the spawner type;
  • withTempDirectory(options, use) — the bracket that reproduces mkdtemp + try { use } finally { rm(dir, { recursive: true, force: true }) } exactly: force, cleanup failure as a typed PlatformError that wins over the operation's failure (as the throwing finally did), cleanup on interruption. Used instead of fs.makeTempDirectoryScoped + Effect.scoped, which the design suggested: the rc.112 finalizer removes without force and orDies, so an operation that deleted its own staging directory would turn a successful call into an ENOENT rejection, and a real cleanup error would surface as the PlatformError wrapper (scope finalizers cannot fail typed). Both Codex review findings;
  • unwrapPlatformError — a PlatformError becomes the NodeJS.ErrnoException it carries, so the two sites keep throwing the identical ENOENT: no such file or directory, mkdtemp ... errors; typed DiagnosticError / CodedError / bare PlatformError pass through;
  • runWithPlatform(effect, options?) = runPromise(Effect.provide(effect, platformLayer).pipe(Effect.mapError(unwrapPlatformError)), options) — the only place the layer is provided. Phase 2 (startDevServer) does makeScopedEffectRuntime(platformLayer) disposed from the session's close and unwrapPlatformError on its programs; nothing else needs to change here.

boundary.ts does not import effect/PlatformError — the first cut did, and every emitted hook wrapper grew by ~12 kB (Data.TaggedError machinery), because boundary.ts is bundled into each hook. The unwrap lives in platform.ts, which emitted artifacts never import; the module comment records why.

Not touched, per the design and the maintainer notes:

  • dev/mcp-session/mcp-session-service.ts:288-370 — ownership of the plugin-data temp dir is transferred to the session; a scoped temp would be removed when the scope closes, i.e. too early. Phase 2, with a session-lifetime scope (or left raw).
  • dev/playground/script-playground-service.ts:139-148 — the finally there distinguishes cleanup failures from run failures in the result object; converting it changes a contract, not just plumbing. Phase 2.
  • test/render.ts — test helper; conventions say don't convert for fixture cleanup alone.
  • src/routes/typegen.ts / src/routes/graph.ts — open in feat(routes): include generated route declarations by default (AB4834); reject duplicated framework plugins in tools.rsbuild (AB4724) #497 (xref-typegen-default); after it merges.
  • everything on the hard keep-raw list (durable-fs and dependents, install/doctor/receipt, IPC inode locks, sync SQLite, chokidar watcher, sync config/discovery, Rspack I/O, emitted shells/installers).

Artifact parity

Rebuilt examples/audiobook-curator (20 files) and examples/host-test (90 files, 60+ hook wrappers) on the #501 head and on this branch, diff -rq. Every hook wrapper, MCP bundle body, bin/*, installer, and manifest field is byte-identical except the pre-existing noise floor: the MCP bundles embed the random .artifact.stage-* staging directory name in a NAMESPACE OBJECT comment, and the manifest's SHA-256 for those files follows. Rebuilding the baseline twice against itself produces the same 3 (audiobook) / 5 (host-test) differing files, so the noise is not this PR's. (That stage-name leak is a reproducibility bug worth a follow-up issue; it is out of scope here.)

Import order in api.ts and codex-plugin-validation.ts matters for that parity: effect/lift.ts's position in the module graph fixes its position in the hook bundles, so the two Effect imports sit after the service imports, with a comment saying so.

Tests

  • tests/support/shared-pack.ts linkWorkspaceTypes: the packed consumer fixtures (packed-consumer.test.ts, public-api-packed.test.ts) symlinked the workspace node_modules/@types directory wholesale; an agent-bundle install now brings @types/ws + @types/node itself, so the helper links entries individually and leaves installed ones alone (this was the Release gates failure on the first CI run).

  • tests/effect-platform.test.ts (new): the layer provides FileSystem + Path; withTempDirectory removes the directory on success, failure and interruption, keeps the result when the operation already removed it, and (over FileSystem.layerNoop) throws the Node cleanup error after a successful operation and lets it win over the operation's failure; unwrapPlatformError unwraps a wrapped ENOENT, keeps a bare PlatformError and a DiagnosticError; runWithPlatform / platformLayer are not public exports of agent-bundle or agent-bundle/dev.

  • tests/codex-plugin-validation.test.ts: the schema-generation test now also asserts the temp directory the validator handed to codex debug schema is gone afterwards.

  • tests/effect-boundary.test.ts: the PlatformError case moved to the platform test (the boundary no longer knows about it).

  • Locally: pnpm typecheck, pnpm lint, pnpm test:unit (3155 passed), example builds above.

Docs

docs/effect-conventions.md: platform.ts added to the boundary-modules section (with the hook-bundle size reason), the withTempDirectory rule replaces the makeTempDirectoryScoped one for library code, the platform-services section records the platform-node (scaffolder) vs platform-node-shared (agent-bundle) split and its footprint reason, and both packages are in the parked-toolchain table and the re-pin chore (re-check the forced redis peer on every re-pin).

Review status

  • Awaiting automated review on the current head.

@changeset-bot

changeset-bot Bot commented Sep 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4e2feb6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
create-agent-bundle Patch
agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-04T03:50:08.631540Z 750f22b Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/effect-filesystem-phase1-tempdirs branch from f2201ea to 6597aa5 Compare September 4, 2026 02:24

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0cffc64bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/agent-bundle/src/api.ts Outdated
Comment on lines +565 to +568
const artifact = yield* fs.makeTempDirectoryScoped({
directory: resolve(options.root),
prefix: '.agent-bundle-artifact-',
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retain forceful cleanup for scoped temp directories

When a generated hook/MCP process or the Codex schema generator removes its staging directory before returning, makeTempDirectoryScoped in the pinned rc.112 implementation finalizes with recursive rm but without force, and converts cleanup failure into a defect. The previous finally used { recursive: true, force: true }, so an already-removed directory preserved the operation result (or the validator's AB6033 diagnostic); this version instead rejects the public call with a PlatformError. Preserve the old force/ENOENT behavior when acquiring the scoped directory; the Codex site at schemaGenerationDiagnostics has the same regression.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed against repos/effect (platform/node-shared/src/NodeFileSystem.ts): the rc.112 finalizer is Effect.orDie(rm(dir, { recursive: true })) with force: false. Fixed in 42305e6: src/effect/platform.ts now exports scopedTempDirectory (makeTempDirectory + acquireRelease finalizer rm(dir, { recursive: true, force: true }), orDie on anything but "already gone", matching the former finally throw), and both temporaryArtifact and schemaGenerationDiagnostics use it. Regression test added in tests/effect-platform.test.ts (operation removes its own directory → call still resolves). docs/effect-conventions.md says to use the helper rather than fs.makeTempDirectoryScoped in agent-bundle and why.

@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/effect-filesystem-phase1 branch from 6628e5b to 68ac2e1 Compare September 4, 2026 02:27
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/effect-filesystem-phase1-tempdirs branch from 6597aa5 to cd89db1 Compare September 4, 2026 02:27
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

@codex review

@pkg-pr-new

pkg-pr-new Bot commented Sep 4, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@508
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@508
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@508

commit: 750f22b

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42305e67a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const fs = yield* FileSystem.FileSystem;
return yield* Effect.acquireRelease(
fs.makeTempDirectory(options),
(directory) => Effect.orDie(fs.remove(directory, { force: true, recursive: true })),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve cleanup errors through the platform boundary

When recursive removal fails for a reason other than an already-missing directory, such as EACCES, EBUSY, or EIO, Effect.orDie converts the PlatformError into a defect before runWithPlatform can apply unwrapPlatformError. A successful public operation therefore rejects with the wrapper instead of the original Node ErrnoException; if the operation also failed, Cause.squash prioritizes that typed failure and hides the cleanup failure, unlike the previous finally { await rm(...) }. Keep cleanup in an explicit exit/cleanup sequence, or otherwise unwrap its failure before defecting, to preserve the prior error contract.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right on both counts — a scope finalizer cannot fail typed, so orDie was the wrong shape. ea01590 replaces scopedTempDirectory with withTempDirectory(options, use), a bracket that reproduces the try/finally literally: makeTempDirectoryEffect.exit(restore(use(dir)))fs.remove(dir, { recursive: true, force: true }) on the typed error channel → yield* exit, under uninterruptibleMask. So a cleanup EACCES after a successful operation reaches runWithPlatform as a typed PlatformError and is unwrapped to the Node ErrnoException; when the operation failed too, the cleanup error wins (it is raised before the operation exit is re-raised), exactly like the throwing finally; and cleanup still runs on interruption. Both call sites use it (no more Effect.scoped). Tests over FileSystem.layerNoop cover the two cleanup-failure orders, plus a real-fs interruption case.

@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: ea01590652

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…d temp directories

Two ordinary temp-directory sites move onto Effect FileSystem's
makeTempDirectoryScoped inside Effect.scoped: api.ts temporaryArtifact
(the throwaway artifact behind listMcp/invokeMcp/runMcp/listHooks/
simulateHook when no --artifact is given) and the Codex validator's
schema-generation output directory. The new src/effect/platform.ts owns
the package's NodeServices layer (runWithPlatform at Promise edges; the
dev server reuses platformLayer through makeScopedEffectRuntime in phase
2), and the boundary unwraps PlatformError to its Node cause so a failed
mkdtemp still throws the same ErrnoException.
…e former try/finally rm

rc.112's makeTempDirectoryScoped finalizes with rm({ recursive: true })
and orDie, so an operation that removed its own staging directory would
reject an already-successful listMcp/invokeMcp/... call or the Codex
validator's AB6033 result with ENOENT at scope close. Both sites now use
scopedTempDirectory (makeTempDirectory + rm({ recursive, force })), with
a regression test.
… exactly

A scope finalizer cannot fail typed, so a cleanup error (EACCES, EBUSY)
surfaced as the PlatformError wrapper after orDie, and when the operation
had failed too Cause.squash preferred the operation's failure where the
former throwing finally reported the cleanup error. withTempDirectory is
a bracket: makeTempDirectory, Effect.exit(use), rm({ recursive, force })
on the typed error channel, then the operation's exit; uninterruptible
around the cleanup. Tests cover both cleanup-failure orders over
FileSystem.layerNoop and cleanup on interruption.
@ScriptedAlchemy
ScriptedAlchemy force-pushed the feat/effect-filesystem-phase1-tempdirs branch from ea01590 to 4e2feb6 Compare September 4, 2026 02:54
@ScriptedAlchemy
ScriptedAlchemy changed the base branch from feat/effect-filesystem-phase1 to main September 4, 2026 02:55
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 4e2feb65d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ed; link workspace @types per entry in packed fixtures

@effect/platform-node@rc.112 declares a non-optional redis peer that npm
auto-installs, and depends on undici and mime: +23 MB / +17 packages in
every consumer install of agent-bundle. platform-node's NodeFileSystem,
NodePath, NodeChildProcessSpawner, NodeStdio, NodeTerminal and NodeCrypto
are re-exports of platform-node-shared, so platformLayer composes the same
NodeServices union from there (+4 MB: @types/node, @types/ws,
undici-types). PlatformServices is derived from the layer.

The packed consumer fixtures symlinked the workspace node_modules/@types
directory wholesale; an agent-bundle install now brings @types/ws and
@types/node, so linkWorkspaceTypes links entries individually and leaves
installed ones alone.
@ScriptedAlchemy

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 750f22bb8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ScriptedAlchemy
ScriptedAlchemy merged commit 8c70ffa into main Sep 4, 2026
13 checks passed
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
… the dependency agent-bundle already carries (#508); drop @effect/platform-node
ScriptedAlchemy added a commit that referenced this pull request Sep 4, 2026
…/Stdio; spell routed-CLI input errors in CLI terms (#465) (#505)

* feat(cli): route first-party CLI terminal I/O through Effect Terminal/Stdio; spell routed-CLI input errors in CLI terms (#465)

* chore(changeset): reference #505

* fix(cli): format doctor host-validation lines through the human formatter after rebase

* fix(cli-entry): keep string-refinement operands and exact lengths in input-issue expectations (review)

* test: link only a missing @types/node into packed-consumer fixtures; assert the #465 flag error in the audiobook-curator dispatch proof

* feat(create-agent-bundle): route --help and flag-error text through Terminal/Stdio at the NodeServices root; port uninstall output to the CLI's Effect services after rebase

* chore(effect): take Terminal/Stdio from @effect/platform-node-shared, the dependency agent-bundle already carries (#508); drop @effect/platform-node
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant